TRPC implementation for VSCode extension message passing
I recently built taffy, an open source github-copilot alternative as a pet project, and during my implementation of the VSCode Extension I realised that a large chunk of the code could be extracted into an easy to use template for making VSCode extensions with better message passing.
You can see a quick demo of what the template looks like below:
This article is the Grandmother story on my thought processes and design decisions behind making this template, if you just want the damn template, click here
If you've ever developed a chrome extension, a VSCode extension, or Figma plugin one of the first problems you'll encounter is that you have to implement some kind of message passing system.
This is because for extension development, there are usually two JS environments - the first being the JS environment for the UI and the second being a sandboxed JS environment (Which we lovingly call the midend
) which can interact with the app you are developing the extension for, be it VSCode, Chrome or Figma.
If you're a filthy JS developer like me, you'll probably find yourself trying to get information from servers at some point via some kind of fetch request to an endpoint, and you'll get a nice response from the server after your function is called, albeit without typescript types.
So when making http requests, there are a few things being done in the background that are make life easy for us
- Endpoints for differentiating requests (Rather than everything going to
/
) - Even when the same endpoint is used twice, the responses are sent to the original requester (i.e. response 3 doesn't go to request 1)
The problem with developing extensions is that we are usually provided with an incredibly barebones messaging passing system
Reinventing the wheel
I will never understand why these assholes won't just give me an easy to use interface, but it is what it is, and I will still have to transfer data.
FireJet (Our Figma-to-Code SaaS) was the first product where we needed this implementation for, so we rolled an internal implementation of a customFetch
function which emulated the fetch
function, except for communicating with the midend
(In Figma's case, the sandboxed JS environment which could edit the figma document), but with full typescript support.
As time passed, eventually our implementation got more and more robust, managing to support sending multiple of the same message at once and also being able to retrieve the response from the midend automatically.
I am an idiot
So after painstakingly making my own implementation as if I was an employee at some high security bank that rolls their own websockets implementation in cobol, I was wondering if there was an easy way to make fetch requests with full typescript support.
That's when I chanced upon TRPC - a backend framework that fully supports end-to-end typesafety, and I immediately started kicking myself for not implementing it earlier.
While it was too late to use TRPC for our backend, I started thinking if it was possible to use it for communication with our midend instead.
Planning the implementation
So the way TRPC works, is that it is agnostic of platform. As long as there is a way to pass messages from the environment A (e.g. Frontend) and environment B (e.g. Midend), you can create your own adapter to be able to use trpc to treat one of those as the 'backend' and the other as the 'frontend'.
I wanted to avoid reinventing the wheel as far as possible, so I tried to simplify the problem as far as possible to a subset where I could do the minimum amount of work to get the job done.
In my head, I was comparing the problem to how TCP works. Essentially, TCP just sends a bunch of header data on messages, and with that header data it is able to make sure data is sent to the correct destination and also an acknowledgement is sent to the original sender.
With that in mind, my plan was:
- Look for an existing open source trpc adapter for chrome extensions
- Commit theft
- Find the part where it sends some kind of
string
from iframe to midend - Replace with the vscode messaging code
- ???
- Profit
Implementing the implementation
Huge shoutout to Janek for letting borrow your code. God bless you, and God bless America for the FREEDOM of the MIT License.
I just realised Janek 'borrowed' his code from jlalmes as well, so God bless you too
Essentially, the entire implementation was the original trpc-chrome
code with just two line changes:
const sendResponse = (response: TRPCChromeResponse["trpc"]) => {
port.postMessage({
trpc: { id, jsonrpc, ...response },
} as TRPCChromeResponse);
panel.webview.postMessage({
trpc: { id: trpc.id, jsonrpc: trpc.jsonrpc, ...response },
} as TRPCVscResponse);
};
I wish I could say that there was more to it, but that's was the bulk of the change. I'm quite sure I also had to make some changes to the receiver functions and the cleanups, but that was essentially the gist of it. I know I built up to this moment with the kind of story you only see before cooking recipes, but unfortunately it is what it is - the majority of software is built standing on the shoulders of giants.
If its any consolation to you, I've extracted the relevant parts of the code into a nice starter template for building your own vscode extension template for those looking for a better, more structured way of writing vscode plugins - which I've made MIT Licensed.
All I ask is that if you use my template, and find it useful, please give a shoutout to me and my blog so I can increase my clout and boost my ego.
Closure
In my head I thought I was pretty cool for implementing an adapter for trpc that made it work for vscode extensions, but after writing this articles I realised that more than anything I was just standing on the shoulders of giants. Huge shoutout to all the people that came before me.
To the people who are reading this article - well you get to reap the rewards of my one line code change. Feel free to use this vscode extension template to make e2e typesafe vscode extensions.